Skip to content
On this page

基本数据处理 · 数组


第 4 节 基本数据处理 · 数组

让我们再次把目光放回我们在第 2 节中提出的大数据中的 “Hello World” 词频统计上。在前面的章节中,我们将 MIT 开源协议中的一部分文本进行了预处理,并将这个文本切割成以字符串为元素的数组。

那么我们就可以开始学习如何处理数组、更强的数组以及使用数组完成我们的案例。

4.1 数组

数组在数学中也可以称为“数列”,也就是以数字或其他类型内容为元素的有序集合。

// 整型数字数组
const intArray = [ 1, 2, 3, 4, 5 ]
// 浮点型数字数组
const floatArray = [ 1.1, 1.2, 1.3, 1.4, 1.5 ]
// 字符串数组
const strArray = [ 'a', 'b', 'c', 'd', 'e' ]

在第 2 节中我们完成的将文本预处理便是将一段较长的文本变成了这种字符串数组。在数据科学领域中,数组可以说是承载了绝大部分数据的表达任务,无论是规整的数据表,还是随时间排序的时间序列,或是复杂多变的非结构化数据,都可以使用数组或类数组的形式表达。

4.1.1 长度

我们前面讲到数组是一个有序集合,那么就意味着它包含了若干个元素。当然了,数组可空。因为它是一个包含了若干元素的集合,所以它就肯定天然地包含了一个属性,那便是元素的数量

const array = [ 1, 2, 3, 4, 5 ]
console.log(array.length) //=> 5

4.1.2 修改内容

因为在计算机中的可用内存是有限的,所以大部分程序在创建数据(比如数组)的时候,都需要先设定好该数据的所占长度。但在 JavaScript 中这并不需要,因为实际在 JavaScript 中数组就是一个特殊的对象,但这并不在讨论范围内。

所以在 JavaScript 中,对数组内容的修改会比较方便。“增查改删”是数据库应用领域中最常见的操作,这在数组中也是一样的。

增加内容

一般来说向数组增加内容是在数组的末端新增内容(Append),当然也可能存在将新内容添加到数组首端或是插入到中间的某一个部分的需求。

添加到末端 Append

Append 操作在 JavaScript 中使用 array.push(element1[, ...[, elementN]]) 方法直接实现。

const array = []

array.push(1)
console.log(array) //=> [1]

array.push(2, 3)
console.log(array) //=> [1, 2, 3]
console.log(array.length) //=> 3

添加到首端 Prepend

添加到首端的操作在 JavaScript 中可以使用 array.unshift(element1[, ...[, elementN]]) 方法。

const array = [ 4, 5 ]

array.unshift(3)
console.log(array) //=> [3, 4, 5]

array.unshift(1, 2)
console.log(array) //=> [1, 2, 3, 4, 5] 

插入到中间某位置 Insert

有的时候我们还需要往数组中的某一个位置添加元素。但需要注意的是,在 JavaScript 中数组元素的位置是从 0 开始的,也就是数组的第一个元素的下标为 0,第二个为 1

假设我们需要在数组 [ 1, 2, 4, 5 ] 中的第三个位置,即下标为 2 的位置上添加元素 3。这需要用到 array.splice(start, deleteCount, element1[, ...[, elementN]]) 方法。你可以注意到该方法第二个参数是 deleteCount,因为这个方法也可以用来删除数组中某一个位置开始的若干个元素,而当我们将这个参数设置为 0 的时候,该方法第三个以及后面的参数便会插入到下标为 start 的位置,后面的元素自动往后推导。

const array = [ 1, 2, 6, 7 ]

array.splice(2, 0, 3)
console.log(array) //=> [1, 2, 3, 6, 7]

array.splice(3, 0, 4, 5)
console.log(array) //=> [1, 2, 3, 4, 5, 6, 7]

查找内容

因为我们说数组是一个有序集合,所以我们在对数组中的元素进行查找的时候也是一个有序进行的过程,而最常用的内容查找方法便是 filter 过滤器。

过滤器的逻辑便是定义一个过滤函数,该函数会有序地被传入数组中当前下标的元素,而它则需要返回该函数是否符合其过滤要求,即结果为 truefalse

假设我们需要在数组 [1, 2, 3, 4, 5, 6, 7, 8] 中找出偶数项,即对元素进行对 2 求余结果为 0 时即为偶数。

const array = [ 1, 2, 3, 4, 5, 6, 7, 8 ]
const evenNumbers = array.filter(function(x) {
  return x % 2 == 0
})

console.log(evenNumbers) //=> [2, 4, 6, 8]

删除内容

删除内容在实际应用中有非常多的含义,有可能是删除不符合某一种条件的元素,那么使用过滤器即可实现;有可能是需要删除某一个位置上的元素,那么就需要使用上面提到的 array.splice(start, deleteCount) 方法。

比如我们要删除数组 [1, 2, 3, 10, 4, 5] 中下标为 3 的元素 10,就可以这样使用,删除从位置 3 开始的 1 个元素。

const array = [1, 2, 3, 10, 4, 5]

array.splice(3, 1)

console.log(array) //=> [1, 2, 3, 4, 5]

更新内容

对数组中的某一个元素进行修改,这种操作与对象中的修改对象属性内容是一样的,因为数组就是一个特殊的对象(属性键为自增长自然数)。

const array = [ 1, 2, 3, 4, 5 ]

array[0] = 10
console.log(array) //=> [10, 2, 3, 4, 5]

“题外话”:封装数组操作工具

虽然绝大多数操作都可以直接使用 JavaScript 中自带的 API 来实现,但是如 array.splice() 这种方法看上去就很容易产生操作错误。那么为了避免开发中的失误,我们可以通过定义一个抽象对象来封装一个用于操作数组的工具库。

const arrayUtils = {
  // methods
}

添加内容

前面我们说道了为数组添加内容有三种模式:末端添加、首端添加和中间插入,那么我们就可以分别为它们封装好 appendprependinsert 函数。

const arrayUtils = {

  // ...
  
  append(array, ...elements) {
    array.push(...elements)
    
    return array
  },
  
  prepend(array, ...elements) {
    array.unshift(...elements)
    
    return array
  },
  
  insert(array, index, ...elements) {
    array.splice(index, 0, ...elements)
    
    return array
  }
}

// 使用
const array = []
arrayUtils.append(array, 3)    // 末端添加元素 3
arrayUtils.prepend(array, 1)   // 首端添加元素 1
arrayUtils.insert(array, 1, 2) // 在位置 1 添加元素 2

console.log(array) //=> [1, 2, 3]

删除内容

因为要删除数组中的某一个元素同样需要用到 array.splice() 方法,为了避免歧义我们也可以将其封装到工具库中。

const arrayUtils = {

  // ...
  
  remove(array, index) {
    array.splice(index, 1)

    return array
  }
}

// 使用
const array = [ 1, 2, 3 ]
arrayUtils.remove(array, 1)

console.log(array) //=> [1, 3]

4.1.3 以数组为单位的基本处理方法

我们前面对数组的介绍,全部都是以元素为单位的操作。但是在大多数情况下,我们都需要以整个数组为单位进行运算,比如进行平均数计算等等。那么我们就需要有一些方法来对整个数组进行处理和计算。

一般来说对数组的总体进行处理可以归类为两个操作:转换和聚合。

转换

转换便是将一个数组中的内容,以一定的方式规律地转换为另一个数组内容。为什么要进行数据转换?因为有时候并不是天生就是可计算的,比如视频、图像、声音和文本等等,而当我们讨论运算的时候,都是以数字为运算的基础。那么为了方便进行运算,就需要先将这些“不可计算”的数据转换为数字,就比如我们前面学习字符串处理的时候就使用过了将英文字母转换为 ASCII 码的过程。

在 JavaScript 中对数组进行“扫描”有不少方法,如前面提到过的 filter、只进行循环的 forEach、与 filter 类似的但只返回第一个匹配值的 find,以及接下来我们需要用到的用于进行数据转换的 map 和用于聚合数据的 reduce

数组转换

假设我们需要将数组 [ 1, 2, 3, 4, 5 ] 中的每一个元素都转换为较其增 2 的数值,也就是说要给每一个元素做 + 2 的操作,那么我们就可以使用 array.map(callback) 方法来实现。

const array = [ 1, 2, 3, 4, 5 ]

const addedArray = array.map(function(x) {
  return x + 2
})

console.log(addedArray) //=> [3,4,5,6,7]

当然我们也可以用来做不同数据类型之间的转换,比如由 ASCII 码组成的数组 [ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100 ],我们需要把它转化为对应的字符串数组就可以这样做。

const asciiArray = [ 72, 101, 108, 108, 111, 87, 111, 114, 108, 100 ]

const charArray = asciiArray.map(function(ascii) {
  return String.fromCharCode(ascii)
})

console.log(charArray) //=> ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]

聚合

什么是聚合?或许我们刚开始听到这个词的时候会“一脸懵逼”,而看到 reduce 这个方法名的时候则更是头疼了。其实不用担心,这个方法在我们很多年前学习加法运算的时候就已经使用过了。不相信吗?我们来接着看。

我们来回忆一下当年我们是怎么一步一步做 1 + 2 + 3 + 4 这道加法运算题的。根据从左到右的运算法则,我们需要首先计算 1 + 2 等于 3;然后将这个和再与 3 相加得到 6,并且以此类推最终得到了这个式子的结果为 10

reduce

而其实这个过程就是 reduce 方法的过程。我们换做使用 JavaScript 来实现便是这样的。

const array = [ 1, 2, 3, 4 ]

const sumResult = array.reduce(function(left, right) {
  return left + right
})

console.log(sumResult) //=> 10

为此我们就可以对这个聚合结果做一个小封装,比如求数组中数值相加的和与相乘的积。

const array = [ 1, 2, 3, 4 ]

function sum(array) {
  return array.reduce(function(left, right) {
    return left + right
  })
}

function multi(array) {
  return array.reduce(function(left, right) {
    return left * right
  })
}

console.log(sum(array))   //=> 10
console.log(multi(array)) //=> 24

甚至我们还可以将这个封装的程度再往抽象的方向进一步发展,这其中涉及了一些函数式编程的概念。

const array = [ 1, 2, 3, 4 ]

function reduceFn(callback) {
  return function(array) {
    return array.reduce(callback)
  }
}

const sum = reduceFn(function(left, right) {
  return left + right
})
const multi = reduceFn(function(left, right) {
  return left * right
})

console.log(sum(array))   //=> 10
console.log(multi(array)) //=> 24

又一个“题外话”:Lodash 工具库

Lodash 是一个包含了非常多实用工具函数的 JavaScript 工具库,其中也包括了非常多我们在对对象型、数组型数据进行处理时需要用到的函数。我们在实际开发中可以借助 Lodash 以大大提高我们的开发效率。

安装 Lodash 工具库的方法有很多,如果你目前正在浏览器环境中使用 JavaScript 进行开发,那么就可以在 HTML 文件的 head 部分中加入以下代码以加载 Lodash 工具库。

<script type="application/javascript" src="https://cdn.staticfile.org/Lodash.js/4.17.5/Lodash.js"></script>

使用 Lodash 实现数组相加

正好我们可以使用 Lodash 来实现我们前面所用到的数组相加。

const array = [ 1, 2, 3, 4 ]

const sumResult = _.sum(array)

console.log(sumResult) //=> 20

是的!Lodash 早就已经为我们提供了这个用于计算数值数组中所有元素相加的函数了。当然,Lodash 的实用性可不止如此,后面我们可以继续来学习。

4.1.4 “更强”的数组

我们前面接触到的数组基本都是只包含了像数值、字符串这样的简单元素。那么如果说数组所包含的元素是更为复杂的对象,甚至是数组呢?实际开发经验告诉我们,除了包含数值、字符串这样的简单数据外,我们还需要“更强”的数组以对付更复杂的需求。

比如我们需要使用一个数组来存储某个部门的人员数据,那么该数组中的元素就应该代表了该部门中的每一个人的抽象映射。而为了能够表达一个人的各种属性,我们需要用对象来完成这样的需求,也就是说我们需要让对象成为数组的元素内容。

const crew = [
  {
    name: 'Peter',
    gender: 'male',
    level: 'Product Manager',
    age: 32
  },
  {
    name: 'Ben',
    gender: 'male',
    level: 'Senior Developer',
    age: 28
  },
  {
    name: 'Jean',
    gender: 'female',
    level: 'Senior Developer',
    age: 26
  },
  {
    name: 'Chang',
    gender: 'male',
    level: 'Developer',
    age: 23
  },
  {
    name: 'Siva',
    gender: 'female',
    level: 'Quality Assurance',
    age: 25
  }
]

而当我们需要表达一个抽象的二维空间(比如数学中的直角坐标系)甚至更高维度空间中的许多点的集合时,每一个点都可以使用一个向量来表示其在对应空间中的位置,比如 [ 3, 5 ]。那么自然地,用于表达这些点的集合的数组就是一个以数组为元素的数组了。

const points = [
  [ 1, 1 ],
  [ 2, 3 ],
  [ 3, 5 ],
  [ 4, 7 ],
  [ 5, 10 ],
  [ 6, 15 ]
]

甚至我们有的时候还需要一个数组中有着不同类型的元素,比如混杂着字符串和数值。

const array = [
  [ 'Hello', 1 ],
  [ 'World', 1 ]
]

这些更复杂的数组有什么实际的用途,我们下一节见分晓。

小结

数组是现实世界中绝大部分数据的主要呈现形式,学会如何灵活地使用数组类型的数据,对数组本身进行测量、对数组中的元素进行操作,那么你就已经可以非常自豪地大声说:我已经踏入了数据科学的大门了!

习题

  1. 将数组 [ 1, 2, 3, 4, 5 ] 转换为 [ 'a1', 'a2', 'a3', 'a4', 'a5' ]
  2. 将数组 [ 1, 2, 3, 4, 5 ] 转换为 [ 'a1', 'b2', 'c3', 'd4', 'e5' ]
  3. 将数组 [ 1, 2, 3, 4, 5 ] 转换为 [ 1, 4, 9, 16, 25 ]
  4. 查询 JavaScript 中 Array.prototype.map 方法的详细文档,并将数组 [ 0, 0, 0, 0, 0 ] 转换为 [ 'A', 'B', 'C', 'D', 'E' ]
  5. 提取数组 [ 1, 2, 3, 4, 5 ] 中的 [ 2, 3, 4 ]